{
  // Define a class
  class Animal {
    name: string;

    constructor(name: string) {
      this.name = name;
    }

    sayHi() {
      return `My name is ${this.name}`;
    }
  }

  let jack = new Animal("Jack");
  console.log(jack.sayHi());

  // Extends a class
  class Cat extends Animal {
    constructor(name: string) {
      super(name);
    }

    sayHi() {
      return "Meow, " + super.sayHi();
    }
  }

  let tom = new Animal("Tom");
  console.log(tom.sayHi());
}

// accessors
{
  class Animal {
    // name: string;

    constructor(name: string) {
      this.name = name;
    }

    get name(): string {
      return this.name;
    }
    set name(value: string) {
      this.name = value;
      console.log("setter: " + value);
    }

    sayHi() {
      return `My name is ${this.name}`;
    }
  }

  let kitty = new Animal("Kitty");
  kitty.name = "Tom";
  console.log(kitty.name);
}

// static methods and static properties
{
  class Animal {
    static num = 42;
    static isAnimal(a: any) {
      return a instanceof Animal;
    }

    name: string;
    constructor(name: string) {
      this.name = name;
    }
  }

  let a = new Animal("Jack");
  Animal.isAnimal(a); // true
  // a.isAnimal(a);
  console.log(Animal.num);
}

// Access modifiers, which are applied to methods/properties
// - public, default and be accessed anywhere
// - private, cannot be accessed outside of itself
// - protected, can be accessed in itself and its sub-class
{
  class Animal {
    public name: string;
    public constructor(name: string) {
      this.name = name;
    }
  }

  let a = new Animal("Jack");
  console.log(a.name); // Jack
  a.name = "Tom";
  console.log(a.name); // Tom
}
{
  class Animal {
    private name: string;
    public constructor(name: string) {
      this.name = name;
    }
  }

  let a = new Animal("Jack");
  // console.log(a.name);
  // a.name = "Tom";
}

{
  class Animal {
    protected name;
    public constructor(name: string) {
      this.name = name;
    }
  }

  class Cat extends Animal {
    constructor(name: string) {
      super(name);
      console.log(this.name);
    }
  }
}

// readonly
{
  class Animal {
    readonly name;
    public constructor(name: string) {
      this.name = name;
    }
  }

  let a = new Animal("Jack");
  console.log(a.name); // Jack
  // a.name = "Tom";
}

// abstract class
{
  abstract class Animal {
    protected name;
    public constructor(name: string) {
      this.name = name;
    }
    public abstract sayHi(): string;
  }

  class Cat extends Animal {
    public sayHi(): string {
      return `Meow, My name is ${this.name}`;
    }
  }

  let cat = new Cat("Tom");
  console.log(cat.sayHi);
}
